#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Test naming occasionally uses camelCase with underscores to separate sections of
// the test name.
#![cfg_attr(test, allow(non_snake_case))]
#![allow(unknown_lints)]
#[macro_use]
extern crate neovide_derive;

#[cfg(target_os = "windows")]
#[cfg(test)]
#[macro_use]
extern crate approx;

#[macro_use]
extern crate clap;

mod bridge;
mod channel_utils;
mod clipboard;
mod cmd_line;
mod dimensions;
mod editor;
mod error_handling;
mod frame;
mod platform;
mod profiling;
mod renderer;
mod running_tracker;
mod settings;
mod units;
mod utils;
mod window;

#[cfg(target_os = "windows")]
mod windows_utils;

#[macro_use]
extern crate derive_new;

use std::{
    env::{self, args},
    fs::{create_dir_all, File, OpenOptions},
    io::Write,
    panic::set_hook,
    process::ExitCode,
    sync::Arc,
    time::{Duration, SystemTime},
};

use anyhow::Result;
use log::trace;
use std::env::var;
use std::panic::PanicHookInfo;
use std::path::PathBuf;
use time::macros::format_description;
use time::OffsetDateTime;
use winit::{error::EventLoopError, event_loop::EventLoopProxy};

#[cfg(not(test))]
use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming};

use backtrace::Backtrace;
use bridge::NeovimRuntime;
use cmd_line::CmdLineSettings;
use error_handling::handle_startup_errors;
use renderer::{
    cursor_renderer::CursorSettings, progress_bar::ProgressBarSettings, RendererSettings,
};
use running_tracker::RunningTracker;
use window::{
    create_event_loop, determine_window_size, UpdateLoop, UserEvent, WindowSettings, WindowSize,
};

pub use channel_utils::*;
#[cfg(target_os = "windows")]
pub use windows_utils::*;

use crate::settings::{load_last_window_settings, Config, PersistentWindowSettings, Settings};

pub use profiling::startup_profiler;

#[cfg(target_os = "macos")]
use crate::frame::Frame;

const DEFAULT_BACKTRACES_FILE: &str = "neovide_backtraces.log";
const BACKTRACES_FILE_ENV_VAR: &str = "NEOVIDE_BACKTRACES";
const REQUEST_MESSAGE: &str = "This is a bug and we would love for it to be reported to https://github.com/neovide/neovide/issues";

fn main() -> ExitCode {
    set_hook(Box::new(|panic_info| {
        let backtrace = Backtrace::new();

        let stderr_msg = generate_stderr_log_message(panic_info, &backtrace);
        eprintln!("{stderr_msg}");

        log_panic_to_file(panic_info, &backtrace, &None);
    }));

    #[cfg(target_os = "windows")]
    {
        windows_fix_dpi();
    }

    // This variable is set by the AppImage runtime and causes problems for child processes
    #[cfg(target_os = "linux")]
    env::remove_var("ARGV0");

    let event_loop = create_event_loop();
    clipboard::init(&event_loop);

    let colorscheme_stream = mundy::Preferences::stream(mundy::Interest::ColorScheme);

    let running_tracker = RunningTracker::new();
    let settings = Arc::new(Settings::new());

    match setup(
        event_loop.create_proxy(),
        running_tracker.clone(),
        settings.clone(),
        colorscheme_stream,
    ) {
        Err(err) => handle_startup_errors(err, event_loop, settings.clone()),
        Ok((window_size, initial_config, runtime)) => {
            let mut update_loop = UpdateLoop::new(
                window_size,
                initial_config,
                event_loop.create_proxy(),
                settings.clone(),
            );

            let result = event_loop.run_app(&mut update_loop);

            // Wait a little bit more and force Nevoim to exit after that.
            // This should not be required, but Neovim through libuv spawns childprocesses that inherits all the handles
            // This means that the stdio and stderr handles are not properly closed, so the nvim-rs
            // read will hang forever, waiting for more data to read.
            // See https://github.com/neovide/neovide/issues/2182 (which includes links to libuv issues)
            runtime.runtime.shutdown_timeout(Duration::from_millis(500));

            match result {
                Ok(_) => running_tracker.exit_code(),
                Err(EventLoopError::ExitFailure(code)) => ExitCode::from(code as u8),
                _ => ExitCode::FAILURE,
            }
        }
    }
}

fn setup(
    proxy: EventLoopProxy<UserEvent>,
    running_tracker: RunningTracker,
    settings: Arc<Settings>,
    colorscheme_stream: mundy::PreferencesStream,
) -> Result<(WindowSize, Config, NeovimRuntime)> {
    //  --------------
    // | Architecture |
    //  --------------
    //
    // BRIDGE:
    //   The bridge is responsible for the connection to the neovim process itself. It is in charge
    //   of starting and communicating to and from the process. The bridge is async and has a
    //   couple of sub components:
    //
    //     NEOVIM HANDLER:
    //       This component handles events from neovim sent specifically to the gui. This includes
    //       redraw events responsible for updating the gui state, and custom neovide specific
    //       events which are registered on startup and handle syncing of settings or features from
    //       the neovim process.
    //
    //     UI COMMAND HANDLER:
    //       This component handles communication from other components to the neovim process. The
    //       commands are split into Serial and Parallel commands. Serial commands must be
    //       processed in order while parallel commands can be processed in any order and in
    //       parallel. `send_ui` is used to send those commands from the window code.
    //
    // EDITOR:
    //   The editor is responsible for processing and transforming redraw events into something
    //   more readily renderable. Ligature support and multi window management requires some
    //   significant preprocessing of the redraw events in order to capture what exactly should get
    //   drawn where. Further this step takes a bit of processing power to accomplish, so it is done
    //   on it's own thread. Ideally heavily computationally expensive tasks should be done in the
    //   editor.
    //
    // RENDERER:
    //   The renderer is responsible for drawing the editor's output to the screen. It uses skia
    //   for drawing and is responsible for maintaining the various draw surfaces which are stored
    //   to prevent unnecessary redraws.
    //
    // WINDOW:
    //   The window is responsible for rendering and gathering input events from the user. This
    //   inncludes taking the draw commands from the editor and turning them into pixels on the
    //   screen. The ui commands are then forwarded back to the BRIDGE to convert them into
    //   commands for neovim to handle properly.
    //
    //  ------------------
    // | Other Components |
    //  ------------------
    //
    // Neovide also includes some other systems which are globally available via lazy static
    // instantiations or passed between components.
    //
    // Settings:
    //   The settings system is live updated from global variables in neovim with the prefix
    //   "neovide". They allow us to configure and manage the functionality of neovide from neovim
    //   init scripts and variables.
    //
    // RunningTracker:
    //   The running tracker responds to quit requests, allowing other systems to trigger a process
    //   exit.
    //
    //  ------------------
    // | Communication flow |
    //  ------------------
    //
    // The bridge reads from Neovim, and sends `RedrawEvent` to the editor. Some events are also
    // sent directly to the window event loop using `WindowCommand`. Finally changed settings are
    // parsed, which are sent as a window event through `SettingChanged`.
    //
    // The editor reads `RedrawEvent` and sends `DrawCommand` to the Window.
    //
    // The Window event loop sends UICommand to the bridge, which forwards them to Neovim. It also
    // reads `DrawCommand`, `SettingChanged`, and `WindowCommand` from the other components.

    settings.register::<WindowSettings>();
    settings.register::<RendererSettings>();
    settings.register::<CursorSettings>();
    settings.register::<ProgressBarSettings>();

    let config = Config::init();
    Config::watch_config_file(config.clone(), proxy.clone());

    set_hook(Box::new({
        let path = config.backtraces_path.clone();
        move |panic_info: &PanicHookInfo<'_>| {
            let backtrace = Backtrace::new();

            let stderr_msg = generate_stderr_log_message(panic_info, &backtrace);
            eprintln!("{stderr_msg}");

            log_panic_to_file(panic_info, &backtrace, &path);
        }
    }));

    //Will exit if -h or -v
    cmd_line::handle_command_line_arguments(args().collect(), settings.as_ref())?;
    {
        let cmdline_settings = settings.get::<CmdLineSettings>();
        if let Some(status) = cmd_line::maybe_passthrough_to_neovim(&cmdline_settings)? {
            std::process::exit(cmd_line::exit_status_code(status));
        }
    }
    #[cfg(not(target_os = "windows"))]
    maybe_disown(&settings);

    startup_profiler();

    #[cfg(not(test))]
    init_logger(&settings);

    trace!("Neovide version: {}", crate_version!());

    // Set BgColor by default when using a transparent frame, so the titlebar text gets correct
    // color.
    #[cfg(target_os = "macos")]
    if settings.get::<CmdLineSettings>().frame == Frame::Transparent {
        let mut window_settings = settings.get::<WindowSettings>();
        window_settings.theme = window::ThemeSettings::BgColor;
        settings.set(&window_settings);
    }

    let window_settings = load_last_window_settings().ok();
    let window_size = determine_window_size(window_settings.as_ref(), &settings);
    let grid_size = match window_size {
        WindowSize::Grid(grid_size) => Some(grid_size),
        // Clippy wrongly suggests to use unwrap or default here
        #[allow(clippy::manual_unwrap_or_default)]
        _ => match window_settings {
            Some(PersistentWindowSettings::Maximized { grid_size, .. }) => grid_size,
            Some(PersistentWindowSettings::Windowed { grid_size, .. }) => grid_size,
            _ => None,
        },
    };

    let mut runtime = NeovimRuntime::new()?;
    runtime.launch(
        proxy,
        grid_size,
        running_tracker,
        settings,
        colorscheme_stream,
    )?;
    Ok((window_size, config, runtime))
}

#[cfg(not(test))]
pub fn init_logger(settings: &Settings) {
    let cmdline_settings = settings.get::<CmdLineSettings>();

    let logger = if cmdline_settings.log_to_file {
        Logger::try_with_env_or_str("neovide")
            .expect("Could not init logger")
            .log_to_file(FileSpec::default())
            .rotate(
                Criterion::Size(10_000_000),
                Naming::Timestamps,
                Cleanup::KeepLogFiles(1),
            )
            .duplicate_to_stderr(Duplicate::Error)
    } else {
        Logger::try_with_env_or_str("neovide = error").expect("Could not init logger")
    };

    logger.start().expect("Could not start logger");
}

#[cfg(not(target_os = "windows"))]
fn maybe_disown(settings: &Settings) {
    use std::process;

    let cmdline_settings = settings.get::<CmdLineSettings>();

    // Never fork unless a tty is attached
    if !cmdline_settings.fork || !utils::is_tty() {
        return;
    }

    if let Ok(current_exe) = env::current_exe() {
        assert!(process::Command::new(current_exe)
            .stdin(process::Stdio::null())
            .stdout(process::Stdio::null())
            .stderr(process::Stdio::null())
            .args(env::args().skip(1))
            .spawn()
            .is_ok());
        process::exit(0);
    } else {
        eprintln!("error in disowning process, cannot obtain the path for the current executable, continuing without disowning...");
    }
}

fn generate_stderr_log_message(panic_info: &PanicHookInfo, backtrace: &Backtrace) -> String {
    if cfg!(debug_assertions) {
        let print_backtrace = match env::var("RUST_BACKTRACE") {
            Ok(x) => x == "full" || x == "1",
            Err(_) => false,
        };

        let backtrace_msg = match print_backtrace {
            true => format!("{backtrace:?}"),
            false => {
                "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace"
                    .to_owned()
            }
        };

        let panic_msg = generate_panic_message(panic_info);

        format!("{panic_msg}\n{REQUEST_MESSAGE}\n{backtrace_msg}")
    } else {
        let panic_msg = generate_panic_message(panic_info);
        format!("{panic_msg}\n{REQUEST_MESSAGE}")
    }
}

fn log_panic_to_file(panic_info: &PanicHookInfo, backtrace: &Backtrace, path: &Option<PathBuf>) {
    let log_msg = generate_panic_log_message(panic_info, backtrace);

    let file_path = match path {
        Some(v) => v,
        None => &match var(BACKTRACES_FILE_ENV_VAR) {
            Ok(v) => PathBuf::from(v),
            Err(_) => settings::neovide_std_datapath().join(DEFAULT_BACKTRACES_FILE),
        },
    };

    if let Some(parent) = file_path.parent() {
        create_dir_all(parent).ok();
    }

    let mut file = match OpenOptions::new()
        .append(true)
        .open(file_path)
        .or_else(|_| File::create(file_path))
    {
        Ok(x) => x,
        Err(e) => {
            eprintln!("Could not create backtraces file. ({e})");
            return;
        }
    };

    match file.write_all(log_msg.as_bytes()) {
        Ok(()) => eprintln!("\nBacktrace saved to {file_path:?}!"),
        Err(e) => eprintln!("Failed writing panic to {file_path:?}: {e}"),
    }
}

fn generate_panic_log_message(panic_info: &PanicHookInfo, backtrace: &Backtrace) -> String {
    let system_time: OffsetDateTime = SystemTime::now().into();

    let timestamp = system_time
        .format(format_description!(
            "[year]-[month]-[day] [hour]:[minute]:[second]"
        ))
        .expect("Failed to parse current time");

    let partial_panic_msg = generate_panic_message(panic_info);
    let full_panic_msg = format!("{timestamp} - {partial_panic_msg}");

    format!("{full_panic_msg}\n{backtrace:?}\n")
}

fn generate_panic_message(panic_info: &PanicHookInfo) -> String {
    // As per the documentation for `.location()`(https://doc.rust-lang.org/std/panic/struct.PanicInfo.html#method.location)
    // the call to location cannot currently return `None`, so we unwrap.
    let location_info = panic_info.location().unwrap();
    let file = location_info.file();
    let line = location_info.line();
    let column = location_info.column();

    let raw_payload = panic_info.payload();

    let payload = match raw_payload
        .downcast_ref::<&str>()
        .map(ToOwned::to_owned)
        // Some panic messages are &str, some are String, try both to see which it is
        .or_else(|| raw_payload.downcast_ref().map(String::as_str))
    {
        Some(msg) => msg.to_owned(),
        None => return "Could not parse panic payload to a string. This is a bug.".to_owned(),
    };

    format!("Neovide panicked with the message '{payload}'. (File: {file}; Line: {line}, Column: {column})")
}
